package paim.wingchun.model.DAO;


import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;

import paim.wingchun.app.Hibernates;
import paim.wingchun.app.Reflections;
import paim.wingchun.model.Recognizable;
import paim.wingchun.model.modelos.Model;
import paim.wingchun.model.pojos.Pojo;

import static paim.wingchun.app.Reflections.getFieldName;

/** Data Access Object */
public class DAO<P extends Pojo> implements Recognizable<P> {

    private static DAO<? extends Pojo> dao;

    protected Class<P> pClass;

    @Override
    public final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /* singleton */
    public static/* synchronized */DAO<? extends Pojo> getInstance() {
        if ( dao == null )
            dao = new DAO<>();
        return dao;
    }

    protected DAO() {}

    /** retorna classe do objeto estado correspondente ao dao/modelo */
    @Override
    public Class<P> getpClass() {
        if ( pClass != null )
            return pClass;

        pClass = Reflections.getClassParameter(getClass(), Pojo.class);
        return pClass;
    }

    public Pojo get(Session session, Class<P> classe, Long id) throws NullPointerException, HibernateException,
                    RuntimeException {

        if ( id == null || id < 0 )
            throw new NullPointerException();

        if ( classe.equals(Pojo.class) )
            throw new RuntimeException("Tentativa de recuperar, um obj da classe " + Pojo.class.getName()
                            + "\n\tCausa provavel: \n" + "\t\tP a ser recuperado nao apresenta um MODELO (? extends "
                            + Model.class.getName() + ")");

        Pojo pojo = (Pojo) session.get(classe, id);
        return pojo;
    }

    /** Metodo que retorna lista contendo todos Pojos do DAO da classe informada.
     *
     * @author paim 25/05/2011
     * @param session
     * @param classP
     * @return List<POJO>
     * @throws HibernateException */
    public List<Pojo> list(Session session, Class<P> classP) throws HibernateException {
        List<Pojo> lista = new ArrayList<Pojo>();
        if ( classP != Pojo.class ) {
            Criteria criteria = session.createCriteria(classP).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
            lista = criteria.list();
        }

        return lista;
    }

    public final List<Pojo> list(Session session, Class<P> classP, int pendencia) throws HibernateException {
        List<Pojo> lista = new ArrayList<Pojo>();

        if ( pendencia > 0 ) {
            StringBuffer consulta = new StringBuffer();
            consulta.append("SELECT objeto FROM objeto IN CLASS ");
            consulta.append(classP.getSimpleName());
            consulta.append(" WHERE objeto.pendencia >= :pendencia");

            Query query = session.createQuery(consulta.toString());
            query.setLong("pendencia", pendencia);

            lista = query.list();
        }
        return lista;
    }

    /** Persiste um Pojo. Atualiza o "id" para um novo pojo. */
    public final Pojo saveOrUpdate(Session session, Pojo pojo) throws NullPointerException, HibernateException {

        if ( pojo == null )
            throw new NullPointerException();

        org.hibernate.Transaction transaction = session.getTransaction();
        transaction.begin();

        try {
            session.saveOrUpdate(pojo);
            session.flush();
            transaction.commit();
        }
        catch ( HibernateException e ) {
            transaction.rollback();
            if ( e instanceof StaleObjectStateException )
                session.clear();
            throw e;
        }

        return pojo;
    }

    /** Persiste uma lista de objetos.
     *
     * @author paim 25/05/2011
     * @param session
     * @param pojos
     * @return List<POJO>
     * @throws HibernateException */
    public final List<Pojo> saveOrUpdate(Session session, List<Pojo> pojos) throws HibernateException {
        org.hibernate.Transaction transaction = session.getTransaction();
        transaction.begin();
        try {
            for ( Pojo pojo : pojos )
                session.saveOrUpdate(pojo);

            session.flush();
            transaction.commit();
        }
        catch ( HibernateException e ) {
            transaction.rollback();
            if ( e instanceof StaleObjectStateException )
                session.clear();
            throw e;
        }
        return pojos;
    }

    public final void delete(Session session, Pojo pojo) throws NullPointerException, HibernateException {
        org.hibernate.Transaction transaction = session.getTransaction();
        transaction.begin();
        if ( pojo == null )
            throw new NullPointerException("pojo == null,  cancelando exclusao");

        if ( pojo.getId() == null )
            throw new NullPointerException("pojo.id == null nao eh  excluido, cancelando exclusao");

        if ( !session.contains(pojo) )
            session.refresh(pojo);

        session.delete(pojo);

        /* comita exclusao se tudo ok */
        try {
            session.flush();
            transaction.commit();
        }
        catch ( HibernateException e ) {
            /* rollback para desfazer transacao */
            transaction.rollback();
            if ( e instanceof StaleObjectStateException )
                session.close();

            throw e;
        }
    }

    public final void delete(Session session, List<Pojo> pojos) throws NullPointerException, HibernateException {
        org.hibernate.Transaction transaction = session.getTransaction();
        transaction.begin();

        try {
            for ( Pojo pojo : pojos ) {
                if ( pojo.getId() == null )
                    throw new NullPointerException("pojo.id == null nao eh  excluido, cancelando exclusao da lista");

                if ( !session.contains(pojo) )
                    session.refresh(pojo);
                session.delete(pojo);
            }
        }
        catch ( NullPointerException e ) {
            transaction.rollback();
            throw e;
        }

        /* comita as exclusoes se tudo ok */
        try {
            session.flush();
            transaction.commit();
        }
        catch ( HibernateException e ) {
            transaction.rollback();
            if ( e instanceof StaleObjectStateException ) {
                session.close();
                Hibernates.openSession();
            }
            throw e;
        }
    }

    public final Pojo reLoad(Session session, Pojo pojo) throws NullPointerException, HibernateException {
        session.evict(pojo);
        pojo = get(session, Hibernate.getClass(pojo), pojo.getId());
        return pojo;
    }

    /** Remove objetos em memoria utilizados pelo banco (Utilizado para liberar memoria) */
    protected void commit(Session sessao) {
        Connection conexao = sessao.connection();
        try {
            Hibernates.defrag(conexao);
            conexao.commit();
        }
        catch ( SQLException e ) {
            try {
                conexao.rollback();
            }
            catch ( SQLException e1 ) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }

    public Object getValorAtributo(Session sessao, Class<? extends Pojo> classePojo, long id, String atributo) {
        StringBuilder consulta = new StringBuilder();

        consulta.append("SELECT objeto.");
        consulta.append(atributo);
        consulta.append(" FROM objeto IN CLASS");
        consulta.append(classePojo.getSimpleName());
        consulta.append(" WHERE objeto.id = :id ");

        Query query = sessao.createQuery(consulta.toString());
        query.setLong("id", id);
        query.setMaxResults(1);

        return query.uniqueResult();
    }

    protected int executeUpdate(Session sessao, Query query) throws Exception {
        org.hibernate.Transaction transaction = sessao.getTransaction();
        transaction.begin();
        int resultado;

        try {
            resultado = query.executeUpdate();
            transaction.commit();
        }
        catch ( HibernateException e ) {
            resultado = -1;
            transaction.rollback();
            if ( e instanceof StaleObjectStateException ) {
                sessao.clear();
                throw new Exception(
                                "Este registro foi alterado enquanto eram feitas estas atualizaï¿½ï¿½es! Ver detalhes.");
            }
            else {
                throw e;
            }
        }

        return resultado;
    }

    // FIXME deveria ter um sort aqui
    public final List<Long> ids(Session session, Class<P> classP) throws HibernateException {
        List<Long> lista = null;
        StringBuffer consulta = new StringBuffer("SELECT objeto.id FROM objeto IN CLASS ");
        consulta.append(classP.getSimpleName());
        Query query = session.createQuery(consulta.toString());
        lista = query.list();
        return lista;
    }

    public final List<Pojo> complementary(Session session, Class<P> classP, List<Long> ids) throws HibernateException {
        if ( ids.isEmpty() )
            return list(session, classP);

        List<Pojo> lista = new ArrayList<Pojo>();
        StringBuffer consulta = new StringBuffer();
        consulta.append("SELECT objeto FROM objeto IN CLASS ");
        consulta.append(classP.getSimpleName());
        consulta.append(" WHERE objeto.id NOT IN (:ids)");

        Query query = session.createQuery(consulta.toString());
        query.setParameterList("ids", ids);

        lista = query.list();
        return lista;
    }

    /** @author paim 09/12/2011
     * @param session
     * @param pojo
     * @return List
     *         <P> */
    @SuppressWarnings("unchecked")
    public List<P> getObjetos(Session session, Pojo pojo) throws Exception {

        String fieldName = getFieldName(pojo.getClass());
        StringBuffer consulta = new StringBuffer();
        consulta.append("SELECT DISTINCT objeto ");
        consulta.append(" FROM objeto IN CLASS " + getpClass().getSimpleName());
        consulta.append(" WHERE objeto." + fieldName + ".id = idPojo");

        Query query = session.createQuery(consulta.toString());
        query.setLong("idPojo", pojo.getId());

        return query.list();
    }
}
